package net.beaconcontroller.loadbalancer;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.Vector;

import org.openflow.protocol.OFFlowMod;
import org.openflow.protocol.OFMatch;
import org.openflow.protocol.OFMessage;
import org.openflow.protocol.OFPacketIn;
import org.openflow.protocol.OFType;
import org.openflow.protocol.action.OFAction;
import org.openflow.protocol.action.OFActionOutput;
import org.openflow.util.HexString;
import org.openflow.util.U16;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.beaconcontroller.core.IBeaconProvider;
import net.beaconcontroller.core.IOFMessageListener;
import net.beaconcontroller.core.IOFSwitch;
import net.beaconcontroller.core.IOFSwitchListener;
import net.beaconcontroller.core.io.OFMessageSafeOutStream;

public class LoadBalancer implements IOFMessageListener, IOFSwitchListener {
    protected static Logger logger = LoggerFactory
            .getLogger(LoadBalancer.class);
    protected IBeaconProvider beaconProvider;

    private final String MININET_IP = "192.168.56.101";
    private final String SERVER_IP = "10.0.0.100";
    private Map<Long, IOFSwitch> switches = new HashMap<Long, IOFSwitch>();
    private ServerLoc serverLoc;
    private Topology topology;

    public IBeaconProvider getBeaconProvider() {
        return beaconProvider;
    }

    public void setBeaconProvider(IBeaconProvider beaconProvider) {
        this.beaconProvider = beaconProvider;
    }

    public void startUp() {
        beaconProvider.addOFMessageListener(OFType.PACKET_IN, this);
        beaconProvider.addOFSwitchListener(this);
        fetchInfo();
        buildTopoState();
    }

    public void shutDown() {
        beaconProvider.removeOFMessageListener(OFType.PACKET_IN, this);
        beaconProvider.removeOFSwitchListener(this);
    }

    @Override
    public String getName() {
        return "loadbalancer";
    }

    class NodeInfo {
        String nodeMac;
        long switchDpid;
        short port;

        public String toString() {
            return "node:" + nodeMac + " switch:" + switchDpid + " port:"
                    + port;
        }
    }

    class ServerLoc {
        public ServerLoc(String data) {
            StringTokenizer st = new StringTokenizer(data, "\n");
            // first entry is for the client
            NodeInfo cn = parseServerLocEntry(st.nextToken());
            client = cn.nodeMac;
            clientLoc = cn;
            // read server locations
            while (st.hasMoreTokens()) {
                NodeInfo n = parseServerLocEntry(st.nextToken());
                loc.put(n.nodeMac, n);
            }
            printServerLoc();
        }

        public HashMap<String, NodeInfo> loc = new HashMap<String, NodeInfo>();
        public String client;
        public NodeInfo clientLoc;

        private NodeInfo parseServerLocEntry(String entry) {
            Scanner s = new Scanner(entry);
            NodeInfo n = new NodeInfo();
            n.nodeMac = s.next();
            n.switchDpid = HexString.toLong(s.next());
            n.port = s.nextShort();
            return n;
        }

        private void printServerLoc() {
            Iterator<String> it = loc.keySet().iterator();
            while (it.hasNext()) {
                String key = it.next();
                NodeInfo n = loc.get(key);
                logger.info(n.nodeMac + " -> " + n.switchDpid + ":" + n.port);
            }
        }

    }

    class Topology {
        public Topology(String data) {
            logger.debug("Creating topology");
            StringTokenizer st = new StringTokenizer(data, "\n");
            parseDpidEntry(st.nextToken());
            while (st.hasMoreTokens()) {
                parseLinkEntry(st.nextToken());
            }
            logger.debug("Done with topology");
            printTopo();
        }

        public Set<Long> getDatapaths() {
            return topo.keySet();
        }

        public Set<Long> getNeighbors(long dpid) {
            return topo.get(dpid).keySet();
        }

        public Vector<Integer> getOutlinks(long dpsrc, long dpdst) {
            if (!topo.containsKey(dpsrc)) {
                return null;
            }
            return topo.get(dpsrc).get(dpdst);
        }

        public boolean isInternal(long dpid, short port) {
            if (!topo.containsKey(dpid)) {
                return false;
            }
            Iterator<Long> it = topo.get(dpid).keySet().iterator();
            while (it.hasNext()) {
                if (topo.get(dpid).get(it.next()).get(0) == port) {
                    return true;
                }
            }
            return false;
        }

        private void parseDpidEntry(String entry) {
            logger.debug("Parsing DPID entry : " + entry);
            StringTokenizer st = new StringTokenizer(entry);
            while (st.hasMoreTokens()) {
                Long dpid = HexString.toLong(st.nextToken());
                if (!topo.containsKey(dpid)) {
                    topo.put(dpid, new HashMap<Long, Vector<Integer>>());
                }
            }
            logger.debug("Total DPIDS parsed : " + topo.keySet().size());
        }

        private void parseLinkEntry(String entry) {
            logger.debug("Parsing Link entry : " + entry);
            StringTokenizer st = new StringTokenizer(entry);
            long dp1 = HexString.toLong(st.nextToken());
            int port1 = Integer.parseInt(st.nextToken());
            long dp2 = HexString.toLong(st.nextToken());
            int port2 = Integer.parseInt(st.nextToken());
            logger.debug("dp1:" + dp1 + " port1:" + port1 + " dp2:" + dp2
                    + " port2:" + port2);

            Vector<Integer> v12 = new Vector<Integer>();
            v12.add(port1);
            v12.add(port2);

            Vector<Integer> v21 = new Vector<Integer>();
            v21.add(port2);
            v21.add(port1);

            if (!topo.containsKey(dp1)) {
                topo.put(dp1, new HashMap<Long, Vector<Integer>>());
            }
            if (!topo.containsKey(dp2)) {
                topo.put(dp2, new HashMap<Long, Vector<Integer>>());
            }

            topo.get(dp1).put(dp2, v12);
            topo.get(dp2).put(dp1, v21);
        }

        private void printTopo() {
            Iterator<Long> it = topo.keySet().iterator();
            while (it.hasNext()) {
                long dpid = it.next();
                String nbrs = "";
                Iterator<Long> nit = topo.get(dpid).keySet().iterator();
                while (nit.hasNext()) {
                    long nbr = nit.next();
                    nbrs += " " + nbr;
                }
                logger.debug(dpid + ":" + nbrs);
            }
        }

        private HashMap<Long, HashMap<Long, Vector<Integer>>> topo = new HashMap<Long, HashMap<Long, Vector<Integer>>>();
    }

    private void fetchInfo() {
        // Fetch client and server information
        String host = MININET_IP;
        int port = 9999;
        String[] args = { "server", "topo" };
        for (int i = 0; i < args.length; i++) {
            logger.debug("Fetching info for " + args[i]);
            try {
                Socket s = new Socket(host, port);
                BufferedReader br = new BufferedReader(new InputStreamReader(
                        s.getInputStream()));
                BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(
                        s.getOutputStream()));
                bw.write(args[i]);
                bw.flush();
                String buf = "";
                while (true) {
                    String line = br.readLine();
                    if (line == null)
                        break;
                    else
                        buf += line + "\n";
                }
                s.close();
                logger.debug("Data fetched:\n" + buf);
                if (args[i] == "server") {
                    serverLoc = new ServerLoc(buf);
                } else if (args[i] == "topo") {
                    topology = new Topology(buf);
                }
            } catch (SocketException e) {
                logger.info("fetchInfo: socket exception" + e);
            } catch (UnknownHostException e) {
                logger.info("fetchInfo: unknown host:" + host);
            } catch (IOException e) {
                logger.info("fetchInfo: I/O exception");
            }
        }
    }

    private void buildTopoState() {
        // build local topology state here
    }

    @Override
    public Command receive(IOFSwitch sw, OFMessage msg) {

        OFPacketIn pi = (OFPacketIn) msg;
        // Build the Match
        OFMatch match = new OFMatch();
        match.loadFromPacket(pi.getPacketData(), pi.getInPort());

        //pega o mac address source
        String mac_address_src_string = HexString.toHexString( match.getDataLayerSource() ); // Convert a string of bytes to a ':' separated hex string
        Integer mac_address_src_key = Arrays.hashCode( match.getDataLayerSource() ); 
        //pega o mac address de destino
        String mac_address_dest_string = HexString.toHexString( match.getDataLayerDestination() ); // Convert a string of bytes to a ':' separated hex string
        Integer mac_address_dest_key = Arrays.hashCode( match.getDataLayerDestination() ); // Creates an integer key from a bytes array.   
          
        
        //verifica se eh p enviar multicast
        if ( mac_address_dest_string.equals("FF:FF:FF:FF:FF:FF")  || mac_address_dest_string.equals("ff:ff:ff:ff:ff:ff") ){
            //pega o primeiro servidor
            Iterator<String> it = serverLoc.loc.keySet().iterator();
            if (it.hasNext()) {
                //pega o servidor
                String key =it.next();
               
                NodeInfo n = serverLoc.loc.get(key);
                match.setDataLayerDestination(n.nodeMac);//coloca no destino do pacote o mac address do servidor
                //sw.getAttributes();
                short outPort = n.port;
                
                OFFlowMod fm = this.write_flow_table(sw, match, outPort, pi);

                 //envia 
                 logger.info("Sending msg : "+mac_address_src_string+" -> "+n.nodeMac);
                 try {
                    sw.getOutputStream().write(fm);
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }                   
                 //this.send_of_msg(sw, (OFMessage) fm);
                 /*
                 OFPacketIn pi2= new OFPacketIn();
                 pi2.setInPort(outPort);
                 pi2.setBufferId(0);
                 OFMatch match2 = new OFMatch();
                
                 OFFlowMod fm2=this.write_flow_table(sw, match2, pi.getInPort(), pi2);
                 try {
                    sw.getOutputStream().write(fm2);
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }            
                */
    
            }
            
          
        }
       
        
        
        return Command.CONTINUE;
    }
    
    
    private OFFlowMod write_flow_table(IOFSwitch sw, OFMatch match, short outPort, OFPacketIn pi) {
        
      
        //cria a entrada na tabela de fluxo
        OFFlowMod fm = (OFFlowMod) sw.getInputStream().getMessageFactory().getMessage(OFType.FLOW_MOD);
        fm.setBufferId(pi.getBufferId());
        match.setInputPort(pi.getInPort());
        match.setWildcards(0);
        fm.setMatch(match);
       
        OFActionOutput action = new OFActionOutput();
        action.setMaxLength((short) 0);
        action.setPort(outPort);
        
        List<OFAction> actions = new ArrayList<OFAction>();
        actions.add(action);
        fm.setActions(actions);
        fm.setLength(U16.t(OFFlowMod.MINIMUM_LENGTH+OFActionOutput.MINIMUM_LENGTH));
     
        return fm;
       
    }
    

    private void send_of_msg(IOFSwitch sw, OFMessage msg) {
        // Send OF message
        try {
            OFMessageSafeOutStream ofsos = sw.getOutputStream();
            ofsos.write(msg);
        } catch (IOException e) {
            logger.info("send_of_msg FAILED!");
            e.printStackTrace();
        }
    }

    @Override
    public void addedSwitch(IOFSwitch sw) {
        logger.info("Registering switch with DPID {}", sw.getId());
        switches.put(sw.getId(), sw);
    }

    @Override
    public void removedSwitch(IOFSwitch sw) {
        logger.info("Unregistering switch with DPID {}", sw.getId());
        switches.remove(sw.getId());
    }

}
